/** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ define([ 'jquery', 'jquery-ui-modules/widget', 'jquery-ui-modules/core', 'jquery/jquery-storageapi', 'mage/mage' ], function ($) { 'use strict'; var hideProps = {}, showProps = {}; hideProps.height = 'hide'; showProps.height = 'show'; $.widget('mage.collapsible', { options: { active: false, disabled: false, collapsible: true, header: '[data-role=title]', content: '[data-role=content]', trigger: '[data-role=trigger]', closedState: null, openedState: null, disabledState: null, ajaxUrlElement: '[data-ajax=true]', ajaxContent: false, loadingClass: null, saveState: false, animate: false, icons: { activeHeader: null, header: null }, collateral: { element: null, openedState: null } }, /** * @private */ _create: function () { this.storage = $.localStorage; this.icons = false; if (typeof this.options.icons === 'string') { this.options.icons = JSON.parse(this.options.icons); } this._processPanels(); this._processState(); this._refresh(); if (this.options.icons.header && this.options.icons.activeHeader) { this._createIcons(); this.icons = true; } this.element.on('dimensionsChanged', function (e) { if (e.target && e.target.classList.contains('active')) { this._scrollToTopIfNotVisible(); } }.bind(this)); this._bind('click'); this._trigger('created'); }, /** * @private */ _refresh: function () { this.trigger.attr('tabIndex', 0); if (this.options.active && !this.options.disabled) { if (this.options.openedState) { this.element.addClass(this.options.openedState); } if (this.options.collateral.element && this.options.collateral.openedState) { $(this.options.collateral.element).addClass(this.options.collateral.openedState); } if (this.options.ajaxContent) { this._loadContent(); } // ARIA (updates aria attributes) this.header.attr({ 'aria-selected': false }); } else if (this.options.disabled) { this.disable(); } else { this.content.hide(); if (this.options.closedState) { this.element.addClass(this.options.closedState); } } }, /** * Processing the state: * If deep linking is used and the anchor is the id of the content or the content contains this id, * and the collapsible element is a nested one having collapsible parents, in order to see the content, * all the parents must be expanded. * @private */ _processState: function () { var anchor = window.location.hash, isValid = $.mage.isValidSelector(anchor), urlPath = window.location.pathname.replace(/\./g, ''), state; this.stateKey = encodeURIComponent(urlPath + this.element.attr('id')); if (isValid && ($(this.content.find(anchor)).length > 0 || this.content.attr('id') === anchor.replace('#', '')) ) { this.element.parents('[data-collapsible=true]').collapsible('forceActivate'); if (!this.options.disabled) { this.options.active = true; if (this.options.saveState) { //eslint-disable-line max-depth this.storage.set(this.stateKey, true); } } } else if (this.options.saveState && !this.options.disabled) { state = this.storage.get(this.stateKey); if (typeof state === 'undefined' || state === null) { this.storage.set(this.stateKey, this.options.active); } else if (state === true) { this.options.active = true; } else if (state === false) { this.options.active = false; } } }, /** * @private */ _createIcons: function () { var icons = this.options.icons; if (icons) { $('') .addClass(icons.header) .attr('data-role', 'icons') .prependTo(this.header); if (this.options.active && !this.options.disabled) { this.header.children('[data-role=icons]') .removeClass(icons.header) .addClass(icons.activeHeader); } } }, /** * @private */ _destroyIcons: function () { this.header .children('[data-role=icons]') .remove(); }, /** * @private */ _destroy: function () { var options = this.options; this.element.removeAttr('data-collapsible'); this.trigger.removeAttr('tabIndex'); if (options.openedState) { this.element.removeClass(options.openedState); } if (this.options.collateral.element && this.options.collateral.openedState) { $(this.options.collateral.element).removeClass(this.options.collateral.openedState); } if (options.closedState) { this.element.removeClass(options.closedState); } if (options.disabledState) { this.element.removeClass(options.disabledState); } if (this.icons) { this._destroyIcons(); } }, /** * @private */ _processPanels: function () { var headers, triggers; this.element.attr('data-collapsible', 'true'); if (typeof this.options.header === 'object') { this.header = this.options.header; } else { headers = this.element.find(this.options.header); if (headers.length > 0) { this.header = headers.eq(0); } else { this.header = this.element; } } if (typeof this.options.content === 'object') { this.content = this.options.content; } else { this.content = this.header.next(this.options.content).eq(0); } // ARIA (init aria attributes) if (this.header.attr('id')) { this.content.attr('aria-labelledby', this.header.attr('id')); } if (this.content.attr('id')) { this.header.attr('aria-controls', this.content.attr('id')); } this.header .attr({ 'role': 'tab', 'aria-selected': this.options.active, 'aria-expanded': this.options.active }); // For collapsible widget only (not tabs or accordion) if (this.header.parent().attr('role') !== 'presentation') { this.header .parent() .attr('role', 'tablist'); } this.content.attr({ 'role': 'tabpanel', 'aria-hidden': !this.options.active }); if (typeof this.options.trigger === 'object') { this.trigger = this.options.trigger; } else { triggers = this.header.find(this.options.trigger); if (triggers.length > 0) { this.trigger = triggers.eq(0); } else { this.trigger = this.header; } } }, /** * @param {jQuery.Event} event * @private */ _keydown: function (event) { var keyCode; if (event.altKey || event.ctrlKey) { return; } keyCode = $.ui.keyCode; switch (event.keyCode) { case keyCode.SPACE: case keyCode.ENTER: this._eventHandler(event); break; } }, /** * @param {jQuery.Event} event * @private */ _bind: function (event) { var self = this; this.events = { keydown: '_keydown' }; if (event) { $.each(event.split(' '), function (index, eventName) { self.events[eventName] = '_eventHandler'; }); } this._off(this.trigger); if (!this.options.disabled) { this._on(this.trigger, this.events); } }, /** * Disable. */ disable: function () { this.options.disabled = true; this._off(this.trigger); this.forceDeactivate(); if (this.options.disabledState) { this.element.addClass(this.options.disabledState); } this.trigger.attr('tabIndex', -1); }, /** * Enable. */ enable: function () { this.options.disabled = false; this._on(this.trigger, this.events); this.forceActivate(); if (this.options.disabledState) { this.element.removeClass(this.options.disabledState); } this.trigger.attr('tabIndex', 0); }, /** * @param {jQuery.Event} event * @private */ _eventHandler: function (event) { if (this.options.active && this.options.collapsible) { this.deactivate(); } else { this.activate(); } event.preventDefault(); }, /** * @param {*} prop * @private */ _animate: function (prop) { var duration, easing, animate = this.options.animate; if (typeof animate === 'number') { duration = animate; } if (typeof animate === 'string') { animate = JSON.parse(animate); } duration = duration || animate.duration; easing = animate.easing; this.content.animate(prop, duration, easing); }, /** * Deactivate. */ deactivate: function () { if (this.options.animate) { this._animate(hideProps); } else { this.content.hide(); } this._close(); }, /** * Force deactivate. */ forceDeactivate: function () { this.content.hide(); this._close(); }, /** * @private */ _close: function () { this.options.active = false; if (this.options.saveState) { this.storage.set(this.stateKey, false); } if (this.options.openedState) { this.element.removeClass(this.options.openedState); } if (this.options.collateral.element && this.options.collateral.openedState) { $(this.options.collateral.element).removeClass(this.options.collateral.openedState); } if (this.options.closedState) { this.element.addClass(this.options.closedState); } if (this.icons) { this.header.children('[data-role=icons]') .removeClass(this.options.icons.activeHeader) .addClass(this.options.icons.header); } // ARIA (updates aria attributes) this.header.attr({ 'aria-selected': 'false', 'aria-expanded': 'false' }); this.content.attr({ 'aria-hidden': 'true' }); this.element.trigger('dimensionsChanged', { opened: false }); }, /** * Activate. * * @return void; */ activate: function () { if (this.options.disabled) { return; } if (this.options.animate) { this._animate(showProps); } else { this.content.show(); } this._open(); }, /** * Force activate. */ forceActivate: function () { if (!this.options.disabled) { this.content.show(); this._open(); } }, /** * @private */ _open: function () { this.element.trigger('beforeOpen'); this.options.active = true; if (this.options.ajaxContent) { this._loadContent(); } if (this.options.saveState) { this.storage.set(this.stateKey, true); } if (this.options.openedState) { this.element.addClass(this.options.openedState); } if (this.options.collateral.element && this.options.collateral.openedState) { $(this.options.collateral.element).addClass(this.options.collateral.openedState); } if (this.options.closedState) { this.element.removeClass(this.options.closedState); } if (this.icons) { this.header.children('[data-role=icons]') .removeClass(this.options.icons.header) .addClass(this.options.icons.activeHeader); } // ARIA (updates aria attributes) this.header.attr({ 'aria-selected': 'true', 'aria-expanded': 'true' }); this.content.attr({ 'aria-hidden': 'false' }); this.element.trigger('dimensionsChanged', { opened: true }); }, /** * @private */ _loadContent: function () { var url = this.element.find(this.options.ajaxUrlElement).attr('href'), that = this; if (url) { that.xhr = $.get({ url: url, dataType: 'html' }, function () { }); } if (that.xhr && that.xhr.statusText !== 'canceled') { if (that.options.loadingClass) { that.element.addClass(that.options.loadingClass); } that.content.attr('aria-busy', 'true'); that.xhr.done(function (response) { setTimeout(function () { that.content.html(response); }, 1); }); that.xhr.always(function (jqXHR, status) { setTimeout(function () { if (status === 'abort') { that.content.stop(false, true); } if (that.options.loadingClass) { that.element.removeClass(that.options.loadingClass); } that.content.removeAttr('aria-busy'); if (jqXHR === that.xhr) { delete that.xhr; } }, 1); }); } }, /** * @private */ _scrollToTopIfNotVisible: function () { if (this._isElementOutOfViewport()) { this.header[0].scrollIntoView(); } }, /** * @private * @return {Boolean} */ _isElementOutOfViewport: function () { var headerRect = this.header[0].getBoundingClientRect(), contentRect = this.content.get().length ? this.content[0].getBoundingClientRect() : false, headerOut, contentOut; headerOut = headerRect.bottom - headerRect.height < 0 || headerRect.right - headerRect.width < 0 || headerRect.left + headerRect.width > window.innerWidth || headerRect.top + headerRect.height > window.innerHeight; contentOut = contentRect ? contentRect.bottom - contentRect.height < 0 || contentRect.right - contentRect.width < 0 || contentRect.left + contentRect.width > window.innerWidth || contentRect.top + contentRect.height > window.innerHeight : false; return headerOut ? headerOut : contentOut; } }); return $.mage.collapsible; });